/**
 * This file is part of aion-emu <aion-emu.com>.
 *
 *  aion-emu is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  aion-emu is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with aion-emu.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aionengine.gameserver.world;

import com.aionengine.gameserver.ai2.event.AIEventType;
import com.aionengine.gameserver.configs.administration.DeveloperConfig;
import com.aionengine.gameserver.configs.main.SiegeConfig;
import com.aionengine.gameserver.configs.main.WorldConfig;
import com.aionengine.gameserver.model.gameobjects.Creature;
import com.aionengine.gameserver.model.gameobjects.StaticDoor;
import com.aionengine.gameserver.model.gameobjects.VisibleObject;
import com.aionengine.gameserver.model.gameobjects.player.Player;
import com.aionengine.gameserver.model.gameobjects.siege.SiegeNpc;
import com.aionengine.gameserver.model.templates.zone.ZoneClassName;
import com.aionengine.gameserver.utils.ThreadPoolManager;
import com.aionengine.gameserver.world.zone.ZoneInstance;
import com.aionengine.gameserver.world.zone.ZoneName;
import javolution.util.FastMap;
import javolution.util.FastMap.Entry;
import org.apache.commons.lang.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Just some part of map.
 *
 * @author -Nemesiss-
 */
public class MapRegion {

    private static final Logger log = LoggerFactory.getLogger(MapRegion.class);

    /**
     * Region id of this map region [NOT WORLD ID!]
     */
    private final int regionId;
    /**
     * WorldMapInstance witch is parent of this map region.
     */
    private final WorldMapInstance parent;
    /**
     * Surrounding regions + self.
     */
    private volatile MapRegion[] neighbours = new MapRegion[0];
    /**
     * Objects on this map region.
     */
    private final FastMap<Integer, VisibleObject> objects = new FastMap<Integer, VisibleObject>().shared();

    private final AtomicInteger playerCount = new AtomicInteger(0);

    private final AtomicBoolean regionActive = new AtomicBoolean(false);

    private final int zoneCount;

    /**
     * Zones in this region
     */
    private FastMap<Integer, TreeSet<ZoneInstance>> zoneMap;

    /**
     * Constructor.
     *
     * @param id
     * @param parent
     */
    MapRegion(int id, WorldMapInstance parent, ZoneInstance[] zones) {
        this.regionId = id;
        this.parent = parent;
        this.zoneCount = zones.length;
        createZoneMap(zones);
        addNeighbourRegion(this);
    }

    /**
     * Return World map id.
     *
     * @return world map id
     */
    public Integer getMapId() {
        return getParent().getMapId();
    }

    /**
     * Return an instance of {@link World}, which keeps map, to which belongs this region
     */
    public World getWorld() {
        return getParent().getWorld();
    }

    /**
     * Returns region id of this map region. [NOT WORLD ID!]
     *
     * @return region id.
     */
    public int getRegionId() {
        return regionId;
    }

    /**
     * Returns WorldMapInstance witch is parent of this instance
     *
     * @return parent
     */
    public WorldMapInstance getParent() {
        return parent;
    }

    /**
     * Returns iterator over AionObjects on this region
     *
     * @return objects iterator
     */
    public FastMap<Integer, VisibleObject> getObjects() {
        return objects;
    }

    public Map<Integer, StaticDoor> getDoors() {
        Map<Integer, StaticDoor> doors = new HashMap<Integer, StaticDoor>();
        for (VisibleObject obj : objects.values()) {
            if (obj instanceof StaticDoor) {
                StaticDoor door = (StaticDoor) obj;
                doors.put(door.getSpawn().getStaticId(), door);
            }
        }
        return doors;
    }

    /**
     * @return the neighbours
     */
    public MapRegion[] getNeighbours() {
        return neighbours;
    }

    /**
     * Add neighbour region to this region neighbours list.
     *
     * @param neighbour
     */
    void addNeighbourRegion(MapRegion neighbour) {
        neighbours = (MapRegion[]) ArrayUtils.add(neighbours, neighbour);
    }

    /**
     * Add AionObject to this region objects list.
     *
     * @param object
     */
    void add(VisibleObject object) {
        if (objects.put(object.getObjectId(), object) == null) {
            if (object instanceof Player) {
                checkActiveness(playerCount.incrementAndGet() > 0);
            } else if (DeveloperConfig.SPAWN_CHECK) {
                Iterator<TreeSet<ZoneInstance>> zoneIter = zoneMap.values().iterator();
                while (zoneIter.hasNext()) {
                    TreeSet<ZoneInstance> zones = zoneIter.next();
                    for (ZoneInstance zone : zones) {
                        if (!zone.isInsideCordinate(object.getX(), object.getY(), object.getZ()))
                            continue;
                        if (zone.getZoneTemplate().getZoneType() != ZoneClassName.DUMMY)
                            return;
                    }
                }
                log.warn("Outside any zones: id=" + object + " > X:" + object.getX() + ",Y:" + object.getY() + ",Z:" + object.getZ());
            }
        }
    }

    /**
     * Remove AionObject from region objects list.
     *
     * @param object
     */
    void remove(VisibleObject object) {
        if (objects.remove(object.getObjectId()) != null)
            if (object instanceof Player) {
                checkActiveness(playerCount.decrementAndGet() > 0);
            }
    }

    final void checkActiveness(boolean active) {
        if (active && regionActive.compareAndSet(false, true)) {
            startActivation();
        } else if (!active) {
            startDeactivation();
        }
    }

    final void startActivation() {
        ThreadPoolManager.getInstance().schedule(new Runnable() {

            @Override
            public void run() {
                log.debug("Activating in map {} region {}", getMapId(), regionId);
                MapRegion.this.activateObjects();
                for (MapRegion neighbor : getNeighbours()) {
                    neighbor.activate();
                }
            }
        }, 1000);
    }

    final void startDeactivation() {
        ThreadPoolManager.getInstance().schedule(new Runnable() {

            @Override
            public void run() {
                log.debug("Deactivating in map {} region {}", getMapId(), regionId);
                for (MapRegion neighbor : getNeighbours()) {
                    if (!neighbor.isNeighboursActive()) {
                        neighbor.deactivate();
                    }
                }
            }
        }, 60000);
    }

    public void activate() {
        if (regionActive.compareAndSet(false, true)) {
            activateObjects();
        }
    }

    /**
     * Send ACTIVATE event to all objects with AI2
     */
    private final void activateObjects() {
        for (VisibleObject visObject : objects.values()) {
            if (visObject instanceof Creature) {
                Creature creature = (Creature) visObject;
                creature.getAi2().onGeneralEvent(AIEventType.ACTIVATE);
            }
        }
    }

    public void deactivate() {
        if (regionActive.compareAndSet(true, false)) {
            deactivateObjects();
        }
    }

    /**
     * Send DEACTIVATE event to all objects with AI2
     */
    private void deactivateObjects() {
        for (VisibleObject visObject : objects.values()) {
            if (visObject instanceof Creature && !(SiegeConfig.BALAUR_AUTO_ASSAULT && visObject instanceof SiegeNpc) && !((Creature) visObject).isFlag()) {
                Creature creature = (Creature) visObject;
                creature.getAi2().onGeneralEvent(AIEventType.DEACTIVATE);
            }
        }
    }

    public boolean isMapRegionActive() {
        return !WorldConfig.WORLD_ACTIVE_TRACE || regionActive.get();
    }

    boolean isNeighboursActive() {
        for (int i = 0; i < neighbours.length; i++) {
            MapRegion r = neighbours[i];
            if (r != null && r.regionActive.get() && r.playerCount.get() > 0)
                return true;
        }
        return false;
    }

    public void revalidateZones(Creature creature) {
        for (Entry<Integer, TreeSet<ZoneInstance>> e = zoneMap.head(), mapEnd = zoneMap.tail(); (e = e.getNext()) != mapEnd; ) {
            boolean foundZone = false;
            int category = e.getKey();
            TreeSet<ZoneInstance> zones = e.getValue();
            for (ZoneInstance zone : zones) {
                if (!creature.isSpawned() || (category != -1 && foundZone)) {
                    zone.onLeave(creature);
                    continue;
                }
                boolean result = zone.revalidate(creature);
                if (!result) {
                    zone.onLeave(creature);
                    continue;
                }
                if (category != -1) {
                    foundZone = true;
                }
                zone.onEnter(creature);
            }
        }
    }

    public List<ZoneInstance> getZones(Creature creature) {
        List<ZoneInstance> z = new ArrayList<ZoneInstance>();
        for (Entry<Integer, TreeSet<ZoneInstance>> e = zoneMap.head(), mapEnd = zoneMap.tail(); (e = e.getNext()) != mapEnd; ) {
            TreeSet<ZoneInstance> zones = e.getValue();
            for (ZoneInstance zone : zones) {
                if (zone.isInsideCreature(creature)) {
                    z.add(zone);
                }
            }
        }
        return z;
    }

    public boolean onDie(Creature attacker, Creature target) {
        for (Entry<Integer, TreeSet<ZoneInstance>> e = zoneMap.head(), mapEnd = zoneMap.tail(); (e = e.getNext()) != mapEnd; ) {
            TreeSet<ZoneInstance> zones = e.getValue();
            for (ZoneInstance zone : zones) {
                if (zone.isInsideCreature(target)) {
                    if (zone.onDie(attacker, target))
                        return true;
                }
            }
        }
        return false;
    }

    public boolean isInsideZone(ZoneName zoneName, float x, float y, float z) {
        for (Entry<Integer, TreeSet<ZoneInstance>> e = zoneMap.head(), mapEnd = zoneMap.tail(); (e = e.getNext()) != mapEnd; ) {
            TreeSet<ZoneInstance> zones = e.getValue();
            for (ZoneInstance zone : zones) {
                if (zone.getZoneTemplate().getName() != zoneName)
                    continue;
                return zone.isInsideCordinate(x, y, z);
            }
        }
        return false;
    }

    public boolean isInsideZone(ZoneName zoneName, Creature creature) {
        for (Entry<Integer, TreeSet<ZoneInstance>> e = zoneMap.head(), mapEnd = zoneMap.tail(); (e = e.getNext()) != mapEnd; ) {
            TreeSet<ZoneInstance> zones = e.getValue();
            for (ZoneInstance zone : zones) {
                if (zone.getZoneTemplate().getName() != zoneName)
                    continue;
                return zone.isInsideCreature(creature);
            }
        }
        return false;
    }

    /**
     * Item use zones always have the same names instances, while we have unique names;
     * Thus, a special check for item use.
     *
     * @param zoneName
     * @param creature
     * @return
     */
    public boolean isInsideItemUseZone(ZoneName zoneName, Creature creature) {
        for (Entry<Integer, TreeSet<ZoneInstance>> e = zoneMap.head(), mapEnd = zoneMap.tail(); (e = e.getNext()) != mapEnd; ) {
            TreeSet<ZoneInstance> zones = e.getValue();
            for (ZoneInstance zone : zones) {
                if (!zone.getZoneTemplate().getXmlName().startsWith(zoneName.toString()))
                    continue;
                if (!zone.isInsideCreature(creature))
                    continue;
                return true;
            }
        }
        return false;
    }

    private void createZoneMap(ZoneInstance[] zones) {
        zoneMap = new FastMap<Integer, TreeSet<ZoneInstance>>();
        for (int i = 0; i < zones.length; i++) {
            ZoneInstance zone = zones[i];
            int category = -1;
            if (zone.getZoneTemplate().getPriority() != 0) {
                category = zone.getZoneTemplate().getZoneType().ordinal();
            }
            TreeSet<ZoneInstance> zoneCategory = zoneMap.get(category);
            if (zoneCategory == null) {
                zoneCategory = new TreeSet<ZoneInstance>();
                zoneMap.put(category, zoneCategory);
            }
            zoneCategory.add(zone);
        }
    }

    public int getZoneCount() {
        return zoneCount;
    }
}
